<template>
  <div ref="baseVirtualScroller" v-if="items.length" class="base-virtual-scroller" :style="virtualScrollerStyle" @scroll="handleScroll">
    <!--  可视内容容器  -->
    <div class="base-virtual-scroller__view-container" :style="viewContainerStyle">
      <template v-for="(item, index) in renderList">
        <VirtualScrollerItem :key="item[keyField]" v-show="item.$$isVisibleForVS" :id="item[keyField]">
          <slot :item="item" :index="index"></slot>
        </VirtualScrollerItem>
      </template>
    </div>
    
    <!-- 滚动条占位符-->
    <div class="base-virtual-scroller__scroll-spacer" :style="scrollSpacerStyle"></div>
  </div>
</template>

<script>

/**
 * 基础虚拟滚动组件
 *  注：
 *    1.外部容器需要有明确的高度或者最大高度
 *    
 *  遗留：
 *    1.滚动定位接口
 */

import VirtualScrollerItem from "./VirtualScrollerItem.vue";

export default {
  name: 'BaseVirtualScroller',
  
  components: {
    VirtualScrollerItem,
  },
  
  provide() {
    this.registerResizeObserverEvent();
    
    return {
      resizeObserver: this.resizeObserver,
    };
  },
  
  props: {
    // 数据源
    items: {
      type: Array,
      required: true,
    },
    
    // 每一项的唯一标识
    keyField: {
      type: String,
      default: 'id',
    },
    
    // 每一项高度的最小尺寸
    itemSize: {
      type: Number,
      default: 30,
    },
    
    // 缓冲区域
    buffer: {
      type: Number,
      default: 0,
    },
  },
  
  data() {
    return {
      renderList: [],  // UI渲染列表
      /***
       *  每项的尺寸位置信息
       *  
       *  @type {Map<string, Object>}
       *  @property {string} documentId - 文档id
       *  @property {Object} info - 尺寸位置信息
       */
      itemsClientRectMap: new Map(),

      startOffset: 0,  // 可视区域容器偏移距离

      totalHeight: 0, // 总高度
      scrollTop: 0, // 滚动的位置
    };
  },

  mounted() {
    this.update();
  },

  activated() {
    this.updateScrollTopForContainerElement();
    this.update();
  },

  computed: {
    virtualScrollerStyle() {
      return {
        height: Math.min(this.totalHeight, this.viewHeight) + 'px'
      }
    },

    viewContainerStyle() {
      return {
        transform: `translate3d(0, ${ this.startOffset }px, 0)`
      }
    },

    scrollSpacerStyle() {
      return {
        height: this.totalHeight + 'px'
      }
    },
  },
  
  watch: {
    items() {
      this.update();
    },
  },

  methods: {
    updateScrollTopForContainerElement() {
      this.$el.scrollTop = this.scrollTop;
    },

    registerResizeObserverEvent() {
      this.resizeObserver = new window.ResizeObserver((entries) => {
        entries.forEach(entry => {
          const target = entry.target;
          const id = target.dataset.id;
          if (!(id === undefined || id === null)) {
            const itemClientRect = this.itemsClientRectMap.get(id);
            itemClientRect.height = Number.parseInt(window.getComputedStyle(target).height) || itemClientRect.height;
          }
        })

        this.update();
      })
    },

    reset() {
      this.renderList = [];
      this.itemsClientRectMap = new Map();
      this.scrollTop = 0;
      this.totalHeight = 0;
      this.viewHeight = 0;
    },

    update() {
      if (this.items.length === 0) {
        this.reset();
        return;
      }
      
      this.$nextTick(() => {
        this.updateItemsClientRectInfo();
        this.updateViewHeight();
        this.updateRenderList();
      })
    },

    updateViewHeight() {
      if (this.viewHeight) {
        return;
      }

      const parentElement = this.$el.parentElement;
      const { height, maxHeight } = window.getComputedStyle(parentElement);

      this.viewHeight = Number.parseInt(height) || Number.parseInt(maxHeight) || 0;

      if (this.viewHeight === 0) {
        console.info(`The external container: ${ parentElement } should have a clear height or maximum height.`);
      }
    },

    updateItemsClientRectInfo() {
      this.updateItemsClientRectMap();

      if(this.items.length <= 0) {
        return;
      }
      const lastItem = this.items[this.items.length - 1];
      let totalHeight = this.itemsClientRectMap.get(lastItem[this.keyField]).bottom ?? 0;
      this.setTotalHeight(totalHeight);
    },

    setTotalHeight(value) {
      this.totalHeight = value;
      this.sendTotalHeight(value);
    },

    sendTotalHeight(value) {
      this.$emit('sendTotalHeight', value);
    },

    updateItemsClientRectMap() {
      const itemsClientRectMap = this.itemsClientRectMap;
      this.itemsClientRectMap = new Map();

      let totalHeight = 0;
      this.items.forEach(item => {
        let info = itemsClientRectMap.get(item[this.keyField]);
        if (!info) {
          info = {
            height: this.itemSize,
            top: 0,
            bottom: 0,
          }
        }

        info.top = totalHeight;
        info.bottom = totalHeight + info.height;
        this.itemsClientRectMap.set(item[this.keyField], info);
        totalHeight += info.height;
      })
    },

    /**
     * 滚动方法，重新计算渲染列表
     * @param e event
     */
    handleScroll() {
      requestAnimationFrame(() => {
        this.scrollTop = this.$refs.baseVirtualScroller.scrollTop;
        this.updateRenderList();
      })
    },

    updateRenderList() {
      if (this.items.length === 0) {
        return;
      }
      
      // 获取渲染的其实位置
      let { start, end } = this.getRenderPositionInfo();

      // 获取渲染项的索引
      let startIndex = this.getRenderIndex(Array.from(this.itemsClientRectMap.values()), start);
      startIndex = startIndex !== -1 ? startIndex : 0;

      let endIndex = this.getRenderIndex(Array.from(this.itemsClientRectMap.values()), end);
      endIndex = endIndex !== -1 ? endIndex : this.items.length - 1;
      
      // 计算偏移值：滚动距离 - （滚动距离 - 渲染列表[0]的top值）
      const startItem = this.items[startIndex];
      this.startOffset = this.scrollTop - (this.scrollTop - this.itemsClientRectMap.get(startItem[this.keyField]).top);

      this.renderList = this.items.map((item, index) => {
        /** 对原始对象添加只读属性isVisibleForVS，控制元素的显隐 */
        item["$$isVisibleForVS"] = index >= startIndex && index <= endIndex;
        return item;
      });
    },

    getRenderPositionInfo() {
      const info = {
        start: this.scrollTop,
        end: this.scrollTop + this.viewHeight,
      }

      info.start -= this.buffer;
      info.end += this.buffer;
      return info;
    },

    /**
     * 获取列表起始或结束索引
     * @param list 总数据
     * @param num 滚动的开始或结束位置值
     * @returns {number}
     */
    getRenderIndex(list, num) {
      let start = 0;
      let end = list.length - 1;
      while (start < end) {
        let middle = Math.floor((start + end) / 2);
        if (list[middle].bottom <= num && list[middle + 1].bottom >= num) {
          return middle + 1;
        } else if (list[middle].bottom > num) {
          end = middle;
        } else if (list[middle + 1].bottom < num) {
          start = middle + 1;
        }
      }

      return -1;
    },
  },
  
  destroyed() {
    this.resizeObserver.disconnect();
  }
};
</script>

<style scoped>
  .base-virtual-scroller {
    width: 100%;
    height: 100%;
    overflow: auto;
    position: relative;
  }
  
  .base-virtual-scroller__scroll-spacer, .base-virtual-scroller__view-container {
    position: absolute;
    left: 0;
    top: 0;
    right: 0;
  }
  
  .base-virtual-scroller__scroll-spacer {
    z-index: -1;
  }
</style>